//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  ExchangeUserCollection.cs
//    
// Abstract:
//
//  This module implements the ExchangeUserCollection class. This class maintains the collection of Enhanced Reminders users.
//  Users are found through Active Directory and are identified by inclusion in a configured DL. This class also implements 
//  the notification web service used by EWS to provide updates for mailbox items with enhanced reminders.
//
//--

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.DirectoryServices;
using System.ServiceModel;
using System.Text;
using System.Threading;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    /// <summary>
    /// Enhanced Reminders users collection. This class also implements the notification web service used by EWS to provide updates.
    /// </summary>
    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
    internal sealed class ExchangeUsers : NotificationServicePortType, IDisposable
    {
        // Address of the notification web service used by EWS to provide updates
        internal static readonly string NotificationUrl = Helpers.GetSetting<string>(Constants.NotificationServiceUrlSetting, String.Empty);

        // Version of the EWS types used by Enhanced Reminders
        internal static readonly RequestServerVersion RequestServerVersion = new RequestServerVersion(ExchangeVersionType.Exchange2007);

        // The WCF host for the notification web service used by EWS to provide updates
        private readonly ServiceHost _host;

        // The collection of users
        private readonly Dictionary<string, ExchangeUserProxy> _currentUsers;

        // Indicates if shutdwon is in progress
        private bool _shuttingDown;

        // Wait event that blocks Dispose until all users have been shutdown
        private ManualResetEvent _shutdownWaitEvent;

        // Timer used to check subscriptions are still alive
        private Timer _subscriptionTimer;

        // Indicates if the timer is already started
        private bool _subscriptionTimerStarted;

        // Indicates if notifications are being received from EWS
        private bool _connected;

        // Synchronizes access to the dictionary
        private readonly object _syncRoot = new object();

        /// <summary>
        /// ctor.
        /// </summary>
        /// <param name="dispatcherProxy">Provides an event that is raised when notifications are delivered (successfully or otherwise)</param>
        /// <remarks>
        /// This method should only be called when the service starts.
        /// </remarks>
        internal ExchangeUsers(DispatcherProxy dispatcherProxy)
        {
            Debug.Assert(dispatcherProxy != null, "dispatcherProxy != null");

            _currentUsers = new Dictionary<string, ExchangeUserProxy>();

            if (String.IsNullOrEmpty(NotificationUrl))
            {
                // Log the error and then throw an exception to ensure that the service doesn't start
                OperationalLogs.LogMissingSettingError(Constants.NotificationServiceUrlSetting);
                throw new ConfigurationErrorsException("The address of the notification web service is not configured");
            }

            Uri resultUri;

            if (!Uri.TryCreate(NotificationUrl, UriKind.Absolute, out resultUri))
            {
                // Log the error and then throw an exception to ensure that the service doesn't start
                OperationalLogs.LogInvalidSettingError(Constants.NotificationServiceUrlSetting);
                throw new ConfigurationErrorsException("The address of the notification web service is invalid");
            }

            _subscriptionTimer = new Timer(new TimerCallback(SubscriptionTimerCallback));

            _connected = true;

            _host = new ServiceHost(this, new Uri(NotificationUrl));

            dispatcherProxy.NotificationReady += DispatcherProxy_NotificationReady;
            dispatcherProxy.NotificationCompleted += DispatcherProxy_NotificationCompleted;
        }

        /// <summary>
        /// Finds and adds potential Enhanced Reminders usesrs.
        /// </summary>
        /// <remarks>
        /// This method searches active directory for users that meet the criteria for enhanced reminders. It only finds potential 
        /// users as it is possible that the users found by active directory won't have Enhanced Reminders search folders.
        /// </remarks>
        internal void AddNewUsers()
        {
            string activeDirectoryRoot = Helpers.GetSetting<string>(Constants.ActiveDirectoryRootSetting, @"LDAP://RootDSE");

            DirectorySearcher searcher = new DirectorySearcher(new DirectoryEntry(activeDirectoryRoot));

            searcher.CacheResults = true;
            // Must be a member of the specified DL to be an Enhanced Reminders user 
            string memberOf = Helpers.GetSetting<string>(Constants.MembershipDLSetting, String.Empty);

            if (String.IsNullOrEmpty(memberOf))
            {
                OperationalLogs.LogMissingSettingError(Constants.MembershipDLSetting);
                throw new ConfigurationErrorsException("Configuration does not specify a membership DL");
            }

            // Set filter to find users with the correct criteria for inclusion
            searcher.Filter = String.Format(null, "(&(objectCategory=person)(objectClass=user)(mail=*)(memberOf={0}))", memberOf);

            SearchResultCollection resultCollection = null;

            try
            {
                // Search for potential users
                resultCollection = searcher.FindAll();
            }
            catch (Exception ex)
            {
                // Log the error and then re-throw to ensure that the service doesn't start
                OperationalLogs.LogActiveDirectorySearchError(activeDirectoryRoot, ex);
                throw;
            }

            foreach (SearchResult result in resultCollection)
            {
                Debug.Assert(result != null, "result != null");

                DirectoryEntry entry = result.GetDirectoryEntry();

                if (entry != null)
                {
                    PropertyValueCollection propertyMail = entry.Properties["mail"];

                    // Must have a mailbox to be an Enhanced Reminders user
                    if (propertyMail != null && propertyMail.Value != null)
                    {
                        PropertyValueCollection propertyName = entry.Properties["displayname"];
                        string principalUserName = ParsePrincipalUserName(entry);

                        if (propertyName != null && propertyName.Value != null)
                        {
                            AddNewUser(propertyMail.Value.ToString(), propertyName.Value.ToString(), principalUserName);
                        }
                        else
                        {
                            AddNewUser(propertyMail.Value.ToString(), propertyMail.Value.ToString(), principalUserName);
                        }
                    }
                }
            }

            if (_currentUsers.Count == 0)
            {
                OperationalLogs.LogNoUsersWarning();
            }
        }

        /// <summary>
        /// Returns the principal user name, which is needed in ExchangeUserProxy to create the impersonation
        /// object used by EWS to give the Scheduler access to a user's mailbox. The principal user name is constructed
        /// by combining the 'samaccountname' and a subset of the 'distingushedname' fields in Active Directory.
        /// </summary>
        private static string ParsePrincipalUserName(DirectoryEntry entry)
        {
            Debug.Assert(entry.Properties["samaccountname"].Value != null, "entry.Properties[\"samaccountname\"].Value != null");
            Debug.Assert(entry.Properties["distinguishedname"].Value != null, "entry.Properties[\"distinguishedname\"].Value != null");

            string samAccountName = entry.Properties["samaccountname"].Value.ToString();
            string[] distinguishedNameFields = entry.Properties["distinguishedname"].Value.ToString().Split(',');
            StringBuilder principalUserName = new StringBuilder(samAccountName + "@");

            for (int index = 0; index < distinguishedNameFields.Length; index++)
            {
                if (distinguishedNameFields[index].StartsWith("DC=", StringComparison.InvariantCultureIgnoreCase) &&
                    distinguishedNameFields[index].Length > 3 /* "DC=".length */)
                {
                    // If the last character is not the @ sign, we need to prepend a period to what we're 
                    // adding.
                    if (principalUserName[principalUserName.Length - 1] != '@')
                    {
                        principalUserName.Append('.');
                    }

                    principalUserName.Append(distinguishedNameFields[index].Substring(3 /* "DC=".Length */));
                }
            }

            return principalUserName.ToString();
        }

        /// <summary>
        /// Starts the WCF host and the initialization process for each user.
        /// </summary>
        /// <remarks>
        /// This method should only be called when the service is starting.
        /// </remarks>
        internal void Open()
        {
            // Start the WCF host for the notification web service
            _host.Open();

            lock (_syncRoot)
            {
                if (_currentUsers.Count > 0)
                {
                    foreach (ExchangeUserProxy user in _currentUsers.Values)
                    {
                        // A null user should never be added to the dictionary
                        Debug.Assert(user != null, "user != null");

                        // Initialize user on a separate thread because initialization is not an asynchronous operation
                        ThreadPool.QueueUserWorkItem(new WaitCallback(InitializeUser), user);
                    }
                }
            }
        }

        /// <summary>
        /// Cleans up each user and the WCF host.
        /// </summary>
        /// <remarks>
        /// This method should only be called when the service is stopping.
        /// </remarks>
        public void Dispose()
        {
            Logger.WriteTraceInformation("Shutting down users...");

            lock (_syncRoot)
            {
                // Dispose should not be called multiple times
                Debug.Assert(!_shuttingDown, "!_shuttingDown");

                // Prevent any new users from being added
                _shuttingDown = true;

                // Dispose of the subsccription timer
                if (_subscriptionTimer != null)
                {
                    _subscriptionTimer.Dispose();
                    _subscriptionTimer = null;
                }

                // Only need to wait if there are users
                if (_currentUsers.Count > 0)
                {
                    _shutdownWaitEvent = new ManualResetEvent(false);
                }

                // Initiate shut down of each user
                foreach (ExchangeUserProxy user in _currentUsers.Values)
                {
                    // A null user should never be added to the dictionary
                    Debug.Assert(user != null, "user != null");
                    user.Shutdown();
                }
            }

            // Don't need notifications from EWS anymore
            _host.Close();

            if (_shutdownWaitEvent != null)
            {
                // Wait for all users to shut down. A timeout on this wait does not need to be specified because the service has a finite shutdown timeout.
                _shutdownWaitEvent.WaitOne();
                _shutdownWaitEvent.Close();
            }

            Logger.WriteTraceInformation("All users have been shut down");
        }

        /// <summary>
        /// Updates the schedule with the specified changes in response to an EWS notification.
        /// </summary>
        /// <param name="request">Specifies the changes</param>
        /// <remarks>
        /// Returning SubscriptionStatusType.Unsubscribe is how subscriptions are cancelled.
        /// </remarks>
        /// <returns>
        /// SubscriptionStatusType.OK if the update was successful; SubscriptionStatusType.Unsubscribe otherwise.
        /// </returns>
        public SendNotificationResponse SendNotification(SendNotificationRequest request)
        {
            lock (_syncRoot)
            {
                if (!_connected)
                {
                    // Connection with EWS has been re-established
                    OperationalLogs.LogConnectionEstablishedInformation();
                    _connected = true;
                }

                // Re-start the subscription timer
                StartSubscriptionTimer();
            }

            // Create the response to the push notification
            SendNotificationResponse result = new SendNotificationResponse(new SendNotificationResultType());

            if (request == null)
            {
                // Discontinue the subscription because this is not a valid notification
                Logger.WriteTraceWarning("Received invalid notification from EWS so unsubscribing.");
                result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.Unsubscribe;
                return result;
            }

            // Get the push notifications
            ResponseMessageType[] responseMessages = request.SendNotification.ResponseMessages.Items;

            // The query string specifies the target mailbox and subscription ID for this notification
            string queryString = OperationContext.Current.IncomingMessageProperties.Via.Query.TrimStart('?');

            if (String.IsNullOrEmpty(queryString))
            {
                // Discontinue the subscription because the query string is invalid
                Logger.WriteTraceWarning("Invalid query string received from EWS so unsubscribing.");
                result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.Unsubscribe;
                return result;
            }

            // The query string should contain two parts; the mailbox name and the subscription ID
            string[] queryStringParts = queryString.Split(';');

            if (queryStringParts.Length != 2)
            {
                // Discontinue the subscription because the query string is invalid
                Logger.WriteTraceWarning("Invalid query string received from EWS so unsubscribing.");
                result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.Unsubscribe;
                return result;
            }

            string mailbox = queryStringParts[0];
            string subscriptionId = queryStringParts[1];

            foreach (SendNotificationResponseMessageType responseMessage in responseMessages)
            {
                if (responseMessage.ResponseCode != ResponseCodeType.NoError)
                {
                    // Discontinue the subscription if there is an error.
                    Logger.WriteTraceWarning("Response code received from EWS indicates an error so unsubscribing. Response Code {0}", responseMessage.ResponseCode);
                    result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.Unsubscribe;
                    return result;
                }

                // Get the notification.
                NotificationType notification = responseMessage.Notification;

                string previousWatermark = notification.PreviousWatermark;
                
                bool success = false;

                Debug.Assert(notification.ItemsElementName.Length == notification.Items.Length, 
                    "notification.ItemsElementName.Length == notification.Items.Length");

                for (int i = 0; i < notification.ItemsElementName.Length; i++)
                {
                    ServerContext context = null;

                    BaseObjectChangedEventType change = notification.Items[i] as BaseObjectChangedEventType;

                    // Only interested in changes to mailbox items
                    if (change != null && change.Item is ItemIdType)
                    {
                        // Create a logging context for this notification from EWS
                        context = new ServerContext(mailbox, Helpers.AssertCast<ItemIdType>(change.Item).Id);

                        switch (notification.ItemsElementName[i])
                        {
                            case ItemsChoiceType.NewMailEvent:
                            case ItemsChoiceType.CreatedEvent:
                                success = OnItemCreated(context, change, mailbox, previousWatermark, subscriptionId);
                                break;

                            case ItemsChoiceType.DeletedEvent:
                                success = OnItemDeleted(context, change, mailbox, previousWatermark, subscriptionId);
                                break;

                            case ItemsChoiceType.CopiedEvent:
                            case ItemsChoiceType.MovedEvent:
                            case ItemsChoiceType.ModifiedEvent:
                                success = OnItemModified(context, change, mailbox, previousWatermark, subscriptionId);
                                break;

                            case ItemsChoiceType.StatusEvent:
                                success = OnStatusUpdated(context, mailbox, change.Watermark, previousWatermark, subscriptionId);
                                break;
                        }
                    }
                    else
                    {
                        // Create a logging context for this notification from EWS
                        context = new ServerContext(mailbox);

                        success = OnStatusUpdated(context, mailbox, notification.Items[i].Watermark, previousWatermark, subscriptionId);
                    }

                    if (!success)
                    {
                        Debug.Assert(context != null, "context != null");

                        // This subscription is out-of-date so unsubscribe
                        Logger.WriteTraceInformation(context, "Ignoring notification from unexpected or out-of-date subscription.");
                        result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.Unsubscribe;
                        return result;
                    }

                    previousWatermark = notification.Items[i].Watermark;
                }
            }

            // Keep this subscription alive
            result.SendNotificationResult.SubscriptionStatus = SubscriptionStatusType.OK;

            return result;
        }

        /// <summary>
        /// Raised when an enhanced reminder mailbox item is deleted.
        /// </summary>
        internal event EventHandler<ItemDeletedEventArgs> ItemDeleted;

        /// <summary>
        /// Raised when an enhanced reminder mailbox item is modified or added.
        /// </summary>
        internal event EventHandler<ItemUpdatedEventArgs> ItemUpdated;

        /// <summary>
        /// Initialize the specified user.
        /// </summary>
        /// <param name="state">User to be initialized</param>
        /// <remarks>
        /// This method enables each user to be initialization on a separate worker pool thread.
        /// </remarks>
        private static void InitializeUser(object state)
        {
            Debug.Assert(state != null, "state != null");

            state.AssertCast<ExchangeUserProxy>().Initialize();
        }

        /// <summary>
        /// Starts the subscription timer. 
        /// </summary>
        /// <remarks>
        /// The timer is used to detect when notifications from EWS stop arriving.
        /// </remarks>
        private void StartSubscriptionTimer()
        {
            // Only start the timer if there are users
            if (_subscriptionTimer != null && _currentUsers.Count > 0)
            {
                // Set the timeout to be twice the subscription timeout for an individual user
                _subscriptionTimer.Change(Monitor.SubscriptionTimeout * 2, Timeout.Infinite);
                _subscriptionTimerStarted = true;
            }
        }

        /// <summary>
        /// Stops the subscription timer.
        /// </summary>
        /// <remarks>
        /// The timer is used to detect when notifications from EWS stop arriving.
        /// </remarks>
        private void StopSubscriptionTimer()
        {
            if (_subscriptionTimer != null)
            {
                // Stop the timer by setting the timeout to infinite
                _subscriptionTimer.Change(Timeout.Infinite, Timeout.Infinite);
                _subscriptionTimerStarted = false;
            }
        }

        /// <summary>
        /// Logs the fact that notifications are not arriving from EWS.
        /// </summary>
        /// <param name="state">Not used</param>
        private void SubscriptionTimerCallback(object state)
        {
            lock (_syncRoot)
            {
                if (!_connected)
                {
                    // Don't log the error more than once
                    return;
                }

                _connected = false;
            }

            OperationalLogs.LogNoNotificationsError(Constants.NotificationServiceUrlSetting);
        }

        /// <summary>
        /// Handles new mailbox item notifications from EWS.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="mailboxName">The name of the mailbox being updated</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">ID for the subscription this update is from</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        private bool OnItemCreated(ServerContext context, BaseObjectChangedEventType change, string mailboxName, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(mailboxName, out user);
            }

            if (user == null)
            {
                // The mailbox does not belong to a current user so unsubscribe for further notifications
                return false;
            }

            return user.OnItemCreated(context, change, previousWatermark, subscriptionId);
        }

        /// <summary>
        /// Handles mailbox item deletion notifications from EWS.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="mailboxName">The name of the mailbox being updated</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">ID for the subscription this update is from</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        private bool OnItemDeleted(ServerContext context, BaseObjectChangedEventType change, string mailboxName, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(mailboxName, out user);
            }

            if (user == null)
            {
                // The mailbox does not belong to a current user so unsubscribe for further notifications
                return false;
            }

            return user.OnItemDeleted(context, change, previousWatermark, subscriptionId);
        }

        /// <summary>
        /// Handles mailbox item modification notifications from EWS.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="change">Description of changed item</param>
        /// <param name="mailboxName">The name of the mailbox being updated</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">ID for the subscription this update is from</param>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        private bool OnItemModified(ServerContext context, BaseObjectChangedEventType change, string mailboxName, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(mailboxName, out user);
            }

            if (user == null)
            {
                // The mailbox does not belong to a current user so unsubscribe for further notifications
                return false;
            }

            return user.OnItemModified(context, change, previousWatermark, subscriptionId);
        }

        /// <summary>
        /// Handles status update notifications from EWS.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="mailboxName">The name of the mailbox being updated</param>
        /// <param name="watermark">Watermark for this update</param>
        /// <param name="previousWatermark">Previous watermark from the previous update</param>
        /// <param name="subscriptionId">ID for the subscription this update is from</param>
        /// <remarks>
        /// This method is invoked periodically by EWS to inform the notifcation service that the subscription is still alive.
        /// </remarks>
        /// <returns>
        /// true if the update is successful; false otherwise
        /// </returns>
        private bool OnStatusUpdated(ServerContext context, string mailboxName, string watermark, string previousWatermark, string subscriptionId)
        {
            Debug.Assert(context != null, "context != null");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(mailboxName, out user);
            }

            if (user == null)
            {
                // The mailbox does not belong to a current user so unsubscribe for further notifications
                return false;
            }

            return user.OnStatusUpdated(context, watermark, previousWatermark, subscriptionId);
        }

        /// <summary>
        /// Adds the specified user.
        /// </summary>
        /// <param name="emailAddress">Mailbox address of the user to be added</param>
        /// <param name="displayName">Display name of the user to be added</param>
        /// <param name="principleUserName">Principle user name of the user to be added</param>
        private void AddNewUser(string emailAddress, string displayName, string principleUserName)
        {
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");
            Debug.Assert(!String.IsNullOrEmpty(displayName), "!String.IsNullOrEmpty(displayName)");
            Debug.Assert(!String.IsNullOrEmpty(principleUserName), "!String.IsNullOrEmpty(principleUserName)");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                if (_shuttingDown)
                {
                    // Already started to shut down so don't add any new users
                    return;
                }

                // Check if there is already a user for this mailbox
                _currentUsers.TryGetValue(emailAddress, out user);
            }

            if (user == null)
            {
                Logger.WriteTraceInformation(Context.Global, "Adding new user, email address: {0}, display name: {1} ", emailAddress, displayName);

                // Create entry for this mailbox
                user = new ExchangeUserProxy(emailAddress, displayName, principleUserName);
                Debug.Assert(user.EmailAddress != null, "user.EmailAddress != null");

                // Attach event handlers
                user.ItemUpdated += User_ItemUpdated;
                user.ItemDeleted += User_ItemDeleted;
                user.InitializeCompleted += User_InitializeCompleted;
                user.Closed += User_Closed;

                lock (_syncRoot)
                {
                    // Add new user to the collection
                    _currentUsers.Add(user.EmailAddress, user);
                }
            }
        }

        /// <summary>
        /// Starts the subscription timer (if it hasn't already been started) when a new user is initialized.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Not used</param>
        private void User_InitializeCompleted(object sender, EventArgs e)
        {
            lock (_syncRoot)
            {
                // Only start the timer if it isn't already started. This is so that as new users are added it doesn't keep 
                // re-starting the timer. The timer is only started after a user has been successfully initialized because 
                // initialization may fail and may take some time to complete.
                if (!_subscriptionTimerStarted && _connected)
                {
                    StartSubscriptionTimer();
                }
            }
        }

        /// <summary>
        /// Handles mailbox item deletion.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Identifies the item that has been deleted</param>
        private void User_ItemDeleted(object sender, ItemDeletedEventArgs e)
        {
            EventHandler<ItemDeletedEventArgs> itemDeleted = ItemDeleted;

            if (itemDeleted != null)
            {
                itemDeleted(this, e);
            }
        }

        /// <summary>
        /// Handles updates to mailbox items.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Identifies the item that has been updated</param>
        private void User_ItemUpdated(object sender, ItemUpdatedEventArgs e)
        {
            EventHandler<ItemUpdatedEventArgs> itemUpdated = ItemUpdated;

            if (itemUpdated != null)
            {
                itemUpdated(this, e);
            }
        }

        /// <summary>
        /// Handles closure of a given user. The user will be removed from the collection.
        /// </summary>
        /// <param name="sender">The user that has been closed</param>
        /// <param name="e">Not used</param>
        private void User_Closed(object sender, EventArgs e)
        {
            ExchangeUserProxy user = sender as ExchangeUserProxy;
            Debug.Assert(user != null, "user != null");
            
            Logger.WriteTraceInformation("Deleting user: " + user.EmailAddress);

            bool userRemoved;

            lock (_syncRoot)
            {
                userRemoved = _currentUsers.Remove(user.EmailAddress);
                Debug.Assert(userRemoved, "userRemoved");

                if (_currentUsers.Count == 0)
                {
                    if (_shutdownWaitEvent != null)
                    {
                        // Shutdown is now complete. This will unblock Dispose and the shutdown of the service.
                        _shutdownWaitEvent.Set();
                    }
                    else
                    {
                        // If the service isn't being shut down, this is probably an error
                        OperationalLogs.LogNoUsersError();
                    }

                    // Stop the timer now that there are no users
                    StopSubscriptionTimer();
                }
            }

            // It is safe to dispose of this ExchangeUserProxy here because this event handler is always invoked on the serialized thread.
            user.Dispose();

            // Detach event handlers
            user.ItemUpdated -= User_ItemUpdated;
            user.ItemDeleted -= User_ItemDeleted;
            user.InitializeCompleted -= User_InitializeCompleted;
            user.Closed -= User_Closed;
        }

        private void DispatcherProxy_NotificationReady(object sender, NotificationReadyEventArgs e)
        {
            Debug.Assert(e != null && e.NotificationItem != null, "e != null && e.NotificationItem != null");

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(e.NotificationItem.EmailAddress, out user);
            }

            if (user != null)
            {
                user.ResolveCustomChannels(e.NotificationItem);
                user.MarkItemAsSent(e.NotificationItem.Context, e.NotificationItem);
            }            
        }

        /// <summary>
        /// Handles confirmation that a notification has been delivered, successfully or otherwise.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="e">Identifies the result of the notification</param>
        private void DispatcherProxy_NotificationCompleted(object sender, NotificationCompletedEventArgs e)
        {
            Debug.Assert(e != null && e.Result != null, "e != null && e.Result != null");

            NotificationId notificationId = e.Result.Id;

            if (notificationId == null)
            {
                // Ignore this notification because it is not valid
                Logger.WriteTraceWarning(e.Result.Context, "Ignoring invalid notification completed response");
                return;
            }

            if (Logger.IsEnabled(LogEntryType.Verbose))
            {
                e.Result.LogTraceInfo();
            }

            ExchangeUserProxy user = null;

            lock (_syncRoot)
            {
                _currentUsers.TryGetValue(notificationId.Name, out user);
            }

            if (user != null)
            {
                // NOTE: Currently we do nothing when a notification response is received. An application could
                // log a successful notification here and attempt to e-mail a user if any channels failed.
            }
        }
    }
}
